package com.pigdroid.adbsqlite.manager;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ADBSQLiteManager implements ConsoleLineListener {


	public static final String KEY_DBNAME = "com.pigdroid.adbsqlite.dbname";
	public static final String KEY_PKG = "com.pigdroid.adbsqlite.pkg";
	public static final String KEY_ADB_ROUTE = "com.pigdroid.adbsqlite.adbRoute";
	public static final String KEY_ERROR_PORT = "com.pigdroid.adbsqlite.errorPort";
	public static final String KEY_OUTPUT_PORT = "com.pigdroid.adbsqlite.outputPort";
	public static final String KEY_COMMAND_PORT = "com.pigdroid.adbsqlite.commandPort";
	private PrintWriter stdin;
	private ScheduledExecutorService scheduler;
	private Process proc;
	
	private Map<String, ConsoleLineListener> listeners = new HashMap<String, ConsoleLineListener>();
	private Properties properties;
	private Socket socket;
	
	private Set<String> pids = new HashSet<String>();

	public ADBSQLiteManager() throws IOException {
		initDefaultProperties();
		init();
	}

	private void initDefaultProperties() {
		this.properties = new Properties();
		properties.put(KEY_COMMAND_PORT, "3333");
		properties.put(KEY_OUTPUT_PORT, "3334");
		properties.put(KEY_ERROR_PORT, "3335");
		properties.put(KEY_ADB_ROUTE, "C:\\etc\\dev\\java\\install\\adt-bundle-windows-x86_64-20130219\\sdk\\platform-tools\\adb.exe");
		properties.put(KEY_PKG, "com.android.browser");
		properties.put(KEY_DBNAME, "browser2.db");
	}
	
	public ADBSQLiteManager(Properties properties) throws IOException {
		initDefaultProperties();
		this.properties.putAll(properties);
		init();
	}
	
	
	public void init() throws IOException {
		int commandPort = Integer.valueOf(properties.getProperty(KEY_COMMAND_PORT));
		configPort(commandPort);
		int errorPort = Integer.valueOf(properties.getProperty(KEY_ERROR_PORT));
		configPort(errorPort);
		int outputPort = Integer.valueOf(properties.getProperty(KEY_OUTPUT_PORT));
		configPort(outputPort);
		runServer(commandPort, outputPort, errorPort);
		PortWorker errorPortWorker = new PortWorker(errorPort, "error", this);
		PortWorker stdoutPortWorker = new PortWorker(outputPort, "stdout", this);

		socket = new Socket();
		socket.connect(new InetSocketAddress("localhost", commandPort));
		stdin = new PrintWriter((socket).getOutputStream());
		configServer();
		scheduler = Executors.newScheduledThreadPool(2);
		scheduler.scheduleAtFixedRate(errorPortWorker, 50, 50,
				TimeUnit.MILLISECONDS);
		scheduler.scheduleAtFixedRate(stdoutPortWorker, 50, 50,
				TimeUnit.MILLISECONDS);
	}

	private void configServer() {
		sendCommand(".headers on");
		sendCommand(".mode csv");
		sendCommand(".stats on");
		sendCommand(".nullvalue *null*");
		sendCommand(".separator |");
	}

	private void runServer(int i, int j, int k) {
		ProcessBuilder builder = new ProcessBuilder(
				properties.getProperty(KEY_ADB_ROUTE),
				"shell");
		try {
			proc  = builder.start();
			PrintWriter printer = new PrintWriter(proc.getOutputStream());
			printer.println("nc -l " + properties.getProperty(KEY_COMMAND_PORT) + " | ( sqlite3 /data/data/"+ properties.getProperty(KEY_PKG) + "/databases/" + properties.getProperty(KEY_DBNAME) 
					+ " | nc -l localhost " + properties.getProperty(KEY_OUTPUT_PORT) + " ) 2>&1 | nc -l localhost " + properties.getProperty(KEY_ERROR_PORT) + " &");
			printer.println("jobs -l");
			printer.println("exit");
			printer.flush();
			Thread.sleep(1000);
			processPIDS(proc.getInputStream());
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		
	}

	private void processPIDS(InputStream in) throws IOException, InterruptedException {
		StringBuffer buff = readBuffer(in);
		StringTokenizer tok = new StringTokenizer(buff.toString(), "\n");
		while(tok.hasMoreTokens()) {
			String line = tok.nextToken().trim();
			if (line.startsWith("[")) {
				StringTokenizer pidTokenizer = new StringTokenizer(line.substring(3), " ");
				while(pidTokenizer.hasMoreTokens()) {
					String val = pidTokenizer.nextToken().trim();
					if (isValidPID(val)) {
						this.pids.add(val);
					}
				}
			}
		}

		//		// Got the pids created, let's find their parents
//		ProcessBuilder builder = new ProcessBuilder(
//				properties.getProperty(KEY_ADB_ROUTE),
//				"shell",
//				"\"ps\"");
//		Process psproc = builder.start();
//		//waitfor hangs... let's wait a few moments
//		//todo remove and wait for stream availability
//		Thread.sleep(500);
//		buff = readBuffer(psproc.getInputStream());
//		tok = new StringTokenizer(buff.toString(), "\n");
//		while(tok.hasMoreTokens()) {
//			processParentPID(tok.nextToken().trim());
//		}
//		psproc.destroy();
		
	}

	private static final boolean isValidPID(String val) {
		int max = val.length();
		boolean ret = true;
		for (int i = 0; i < max; i++) {
			if (!Character.isDigit(val.charAt(i))) {
				ret = false;
				break;
			}
		}
		return ret;
	}

	private void processParentPID(String line) {
			StringTokenizer tok = new StringTokenizer(line, " ");
			tok.nextToken();
			String foundId = tok.nextToken().trim();
			if (pids.contains(foundId)) {
				foundId = tok.nextToken();
				if (!"1".equals(foundId)) {
					pids.add(foundId);
				}
			}
	}

	private StringBuffer readBuffer(InputStream in) throws IOException {
		StringBuffer buff = new StringBuffer();
		while (in.available() > 0) {
			buff.append((char) in.read());
		}
		return buff;
	}

	private void configPort(int i) {
		ProcessBuilder builder = new ProcessBuilder(
				properties.getProperty(KEY_ADB_ROUTE),
				"forward",
				"tcp:" + i,
				"tcp:" + i);
		try {
			builder.start();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}

	}

	public void onLineReceived(String portName, String command) {
		ConsoleLineListener consoleLineListener = listeners.get(portName);
		if (consoleLineListener != null) {
			consoleLineListener.onLineReceived(portName, command);
		}
	}

	public void sendCommand(String cmd) {
		if (cmd.startsWith(".")) {
			stdin.println(cmd);
		} else {
			stdin.println(cmd + ";");
		}
		stdin.flush();
	}
	
	public void registerStdoutListener(ConsoleLineListener consoleLineListener) {
		listeners.put("stdout", consoleLineListener);
	}

	public void registerErrorListener(ConsoleLineListener consoleLineListener) {
		listeners.put("error", consoleLineListener);
		
	}
	public String getSchema() {
		return properties.getProperty(KEY_DBNAME);
	}

	public void close() throws IOException, InterruptedException {
		killPIDS();
		stdin.close();
		stdin = null;
		socket.close();
		proc.destroy();
		Thread.sleep(1000); //seems like nc lasts a few more millis after killing
	}

	private void killPIDS() throws IOException, InterruptedException {
			for (String pid : pids) {
				killPID(pid);
			}
	}

	private void killPID(String pid) throws InterruptedException, IOException {
		ProcessBuilder builder = new ProcessBuilder(
				properties.getProperty(KEY_ADB_ROUTE),
				"shell",
				"\"kill -9 " + pid + "\"");
		builder.start().waitFor();
		proc.destroy();
	}

}
